#include "PrecompiledHeader.h"
#include "Material.h"

/*--------------------------------------------------------------------------*/

Material::Material(const std::string& matfile) :
	m_diffuseColor(glm::vec3(1.0f, 1.0f, 1.0f)),
	m_normalMap_strength(300.0f),
	m_specularShininess(100.0f)
{
	this->LoadMaterialFromFile(matfile);
}

/*--------------------------------------------------------------------------*/

Material::~Material()
{
	for (auto i = this->m_techniques.begin(); i != this->m_techniques.end(); ++i)
	{
		ROpenGL::DeleteProgram(&i->m_program);
	}
}

/*--------------------------------------------------------------------------*/

const std::string& Material::GetName() const
{
	return this->m_name;
}

/*--------------------------------------------------------------------------*/

GLProgram* Material::GetProgram(EMaterialTechnique::Type v) const
{
	for (auto iter = this->m_techniques.begin(); iter != this->m_techniques.end(); ++iter)
	{
		if (iter->m_techniqueType == v)
		{
			return iter->m_program;
		}
	}

	return NULL;
}

/*--------------------------------------------------------------------------*/

const glm::vec3& Material::GetDiffuseColor() const
{
	return this->m_diffuseColor;
}

/*--------------------------------------------------------------------------*/

const float& Material::GetSpecularShininess() const
{
	return this->m_specularShininess;
}

/*--------------------------------------------------------------------------*/

const GLTextureMap& Material::GetTextures() const
{
	return this->m_textures;
}

/*--------------------------------------------------------------------------*/

const float& Material::GetNormalMapStrength() const
{
	return this->m_normalMap_strength;
}

/*--------------------------------------------------------------------------*/

void Material::LoadMaterialFromFile(const std::string& path)
{
	pugi::xml_document mfile;
	AssertLogC(mfile.load_file(path.c_str()) == false, "File %s not found!", path.c_str());

	pugi::xml_node material = mfile.child("material");
	this->m_name = material.attribute("name").as_string("");

	AssertLogC(this->m_name.empty(), "Name not specified in %s", path.c_str());

	/*---------Context----------------------------------------------------------*/

	for (pugi::xml_node contextNode = material.child("context"); contextNode; contextNode = contextNode.next_sibling("context"))
	{
		std::string name = contextNode.attribute("name").as_string("");
		std::string import = contextNode.attribute("import").as_string("");

		AssertLogC(name.empty() && import.empty(), "Both name and import atrributes are empty in context definition in %s", this->m_name.c_str());
		AssertLogC(!name.empty() && !import.empty(), "Both name and import attributes have value in %s", this->m_name.c_str());

		const Context* importedContext = g_Material->GetContext(import);

		if (importedContext == NULL)
		{
			Context* context = g_Material->CreateNewContext(name);
			this->LoadContext(contextNode, context);
		}
	}

	/*----Technique-------------------------------------------------------------*/

	for (pugi::xml_node techniqueNode = material.child("technique"); techniqueNode; techniqueNode = techniqueNode.next_sibling("technique"))
	{
		this->LoadTechniques(techniqueNode);
	}

	const uint numTechniques = this->m_techniques.size();

	/*-------------Loading shaders----------------------------------------------*/

	for (uint techID = 0; techID < numTechniques; ++techID)
	{
		/*--------------------------------------------------------------------------*/
		/*-------Shaders------------------------------------------------------------*/

		for (pugi::xml_node shaderNode = material.child("shader"); shaderNode; shaderNode = shaderNode.next_sibling("shader"))
		{
			std::string import = shaderNode.attribute("import").as_string("");

			GLShader* importedShader = ROpenGL::GetShader(import);

			if (importedShader == NULL)
			{
				this->LoadShader(shaderNode, this->m_techniques[techID]);
			}
			else
			{
				this->m_techniques[techID].m_shaders.push_back(importedShader);
			}
		}

		/*--------------------------------------------------------------------------*/
		/*--------------------------------------------------------------------------*/
	}

	/*---------Attaching and linking shaders to program-------------------------*/

	for (uint techID = 0; techID < numTechniques; ++techID)
	{
		uint numShaders = this->m_techniques[techID].m_shaders.size();

		if (numShaders)
		{
			GLProgram* program = ROpenGL::GenProgram(this->m_techniques[techID].m_techniqueMacroName + "_" + this->m_name);

			for (uint i = 0; i < numShaders; ++i)
			{
				ROpenGL::AttachShader(program, this->m_techniques[techID].m_shaders[i]);
			}

			ROpenGL::Link(program);
			this->m_techniques[techID].m_program = program;
		}
	}
}

/*--------------------------------------------------------------------------*/

void Material::LoadContext(pugi::xml_node& contextNode, Context* context)
{
	if (contextNode.child("depth_testing").attribute("enabled").as_bool(false))
	{
		context->m_flags |= EContextFlag::DepthTesting;
	}

	if (contextNode.child("face_culling").attribute("enabled").as_bool(false))
	{
		context->m_flags |= EContextFlag::Culling;
		context->m_cullingMode = ROpenGL::StrToEnum(contextNode.child("face_culling").attribute("face").as_string("GL_NONE"));
	}

	if (contextNode.child("blending").attribute("enabled").as_bool(false))
	{
		context->m_flags |= EContextFlag::Blending;
		context->m_blendSrc = ROpenGL::StrToEnum(contextNode.child("blending").attribute("sfactor").as_string("GL_SRC_ALPHA"));
		context->m_blendDst = ROpenGL::StrToEnum(contextNode.child("blending").attribute("dfactor").as_string("GL_ONE_MINUS_SRC_ALPHA"));
	}

	context->m_frontPolygonMode = ROpenGL::StrToEnum(contextNode.child("polygon_mode").attribute("front").as_string("GL_FILL"));
	context->m_backPolygonMode = ROpenGL::StrToEnum(contextNode.child("polygon_mode").attribute("back").as_string("GL_FILL"));

	if (contextNode.child("write_depth").attribute("enabled").as_bool(true))
	{
		context->m_flags |= EContextFlag::WriteDepth;
	}

	if (contextNode.child("write_color").attribute("enabled").as_bool(true))
	{
		context->m_flags |= EContextFlag::WriteColor;
	}

	if (contextNode.child("write_stencil").attribute("enabled").as_bool(true))
	{
		context->m_flags |= EContextFlag::WriteStencil;
	}
}

/*--------------------------------------------------------------------------*/

static EMaterialTechnique::Type Convert(const std::string& str)
{
	if (str == "DEFAULT")
	{
		return EMaterialTechnique::Default;
	}
	else if (str == "INSTANCED")
	{
		return EMaterialTechnique::Instanced;
	}
	else if (str == "FONT_NORMAL")
	{
		return EMaterialTechnique::Font_Normal;
	}
	else if (str == "FONT_OUTLINE")
	{
		return EMaterialTechnique::Font_Outline;
	}
	else if (str == "PRIMITIVE_NOTEXTURE")
	{
		return EMaterialTechnique::Primitive_NoTexture;
	}
	else if (str == "DEFAULT_3D")
	{
		return EMaterialTechnique::Default3D;
	}
	else if (str == "PRIMITIVE_NOTEXTURE_3D")
	{
		return EMaterialTechnique::Primitive_NoTexture_3D;
	}
	else
	{
		return EMaterialTechnique::None;
	}
}

void Material::LoadTechniques(pugi::xml_node& techniqueNode)
{
	Technique newTechnique;
	newTechnique.m_techniqueMacroName = techniqueNode.attribute("name").as_string("UNKNOWN");
	newTechnique.m_techniqueType = Convert(newTechnique.m_techniqueMacroName);
	newTechnique.m_context = g_Material->GetContext(techniqueNode.attribute("context").as_string(""));

	AssertLogC(newTechnique.m_context == NULL, "Technique %s does not have context", newTechnique.m_techniqueMacroName.c_str());

	this->m_techniques.push_back(newTechnique);
}

/*--------------------------------------------------------------------------*/

void Material::LoadShader(pugi::xml_node& shaderNode, Technique& technique)
{
	std::string name = shaderNode.attribute("name").as_string("");
	std::string type = shaderNode.attribute("type").as_string("GL_VERTEX_SHADER");

	AssertLogC(name.empty(), "Empty shader name!");

	GLenum shadertype = ROpenGL::StrToEnum(type);

	/*--------------------------------------------------------------------------*/
	std::string glslSource;

	for (pugi::xml_node glsl = shaderNode.child("glsl"); glsl; glsl = shaderNode.next_sibling("glsl"))
	{
		glslSource += std::string(glsl.child_value()) + std::string("\n");
	}

	/*--------------------------------------------------------------------------*/

	name = technique.m_techniqueMacroName + "_" + name;
	GLShader* glshader = ROpenGL::GenShader(name, shadertype, glslSource);

	/*--------------------------------------------------------------------------*/
	/*----INPUT-----------------------------------------------------------------*/
	for (pugi::xml_node input = shaderNode.child("input"); input; input = shaderNode.next_sibling("input"))
	{
		/*----MACRO-----------------------------------------------------------------*/
		ROpenGL::BindDefineToShader(glshader, "TECHNIQUE_" + technique.m_techniqueMacroName, "1");

		for (pugi::xml_node macro = input.child("macro"); macro; macro = macro.next_sibling("macro"))
		{
			std::string name = macro.attribute("name").as_string("undefined");
			std::string value = macro.attribute("value").as_string("undefined");

			ROpenGL::BindDefineToShader(glshader, name, value);
		}

		/*-------UNIFORMBUFFERS-----------------------------------------------------*/
		for (pugi::xml_node ubo = input.child("ubo"); ubo; ubo = ubo.next_sibling("ubo"))
		{
			std::string name = ubo.attribute("name").as_string("undefined");
			std::string exTechnique = ubo.attribute("technique").as_string("");

			if (exTechnique.empty() || exTechnique == technique.m_techniqueMacroName)
			{
				GLUniformBuffer* gluniformbuffer = ROpenGL::GetUniformBuffer(name);
				ROpenGL::BindUBOToShader(glshader, gluniformbuffer);
			}
		}

		/*---SAMPLERS---------------------------------------------------------------*/
		for (pugi::xml_node sampler = input.child("sampler"); sampler; sampler = sampler.next_sibling("sampler"))
		{
			std::string name = sampler.attribute("name").as_string("undefined");
			std::string exTechnique = sampler.attribute("technique").as_string("");

			if (exTechnique.empty() || exTechnique == technique.m_techniqueMacroName)
			{
				std::string textureName = sampler.attribute("texture").as_string("");

				if (textureName.empty())
				{
					textureName = name;
				}

				std::string samplerName = sampler.attribute("sampler").as_string("");

				UniformSampler us(name, textureName, samplerName);
				technique.m_samplers.push_back(us);

				ROpenGL::BindSamplerToShader(glshader, name);
			}
		}


		/*------DATA----------------------------------------------------------------*/
		for (pugi::xml_node data = input.child("data"); data; data = data.next_sibling("data"))
		{
			std::string type = data.attribute("type").as_string("undefined");
			std::string name = data.attribute("name").as_string("undefined");
			int location = data.attribute("location").as_int(-1);
			std::string exTechnique = input.attribute("technique").as_string("");

			if (exTechnique.empty() || exTechnique == technique.m_techniqueMacroName)
			{
				ROpenGL::BindInputToShader(glshader, type, name, location);
			}
		}

		/*------VERTEXDECLARATION---------------------------------------------------*/
		for (pugi::xml_node vd = input.child("vertex_declaration"); vd; vd = vd.next_sibling("vertex_declaration"))
		{
			std::string value = vd.attribute("value").as_string("");
			std::string exTechnique = input.attribute("technique").as_string("");

			if (exTechnique.empty() || exTechnique == technique.m_techniqueMacroName)
			{
				uint length = value.length();

				std::string semanticName;
				std::string glslName;
				bool semanticReading = true;

				for (uint i = 0; i < length; ++i)
				{
					if (value[i] != '=' && value[i] != '|')
					{
						if (semanticReading)
						{
							semanticName += value[i];
						}
						else
						{
							glslName += value[i];
						}
					}
					else if (value[i] == '=')
					{
						semanticReading = false;
					}
					else if (value[i] == '|')
					{
						semanticReading = true;

						std::string name = glslName;
						std::string type = ROpenGL::GetSemanticType(semanticName);
						int location = ROpenGL::GetSemanticLocation(semanticName);

						ROpenGL::BindInputToShader(glshader, type, name, location);

						semanticName = "";
						glslName = "";
					}
				}

				if (semanticName.empty() == false && glslName.empty() == false)
				{
					std::string name = glslName;
					std::string type = ROpenGL::GetSemanticType(semanticName);
					int location = ROpenGL::GetSemanticLocation(semanticName);

					ROpenGL::BindInputToShader(glshader, type, name, location);
				}
			}
		}
	}

	/*----OUTPUT----------------------------------------------------------------*/
	for (pugi::xml_node output = shaderNode.child("output"); output; output = output.next_sibling("output"))
	{
		/*------DATA----------------------------------------------------------------*/
		for (pugi::xml_node data = output.child("data"); data; data = data.next_sibling("data"))
		{
			std::string type = data.attribute("type").as_string("undefined");
			std::string name = data.attribute("name").as_string("undefined");
			int location = data.attribute("location").as_int(-1);
			std::string exTechnique = output.attribute("technique").as_string("");

			if (exTechnique.empty() || exTechnique == technique.m_techniqueMacroName)
			{
				ROpenGL::BindOutputToShader(glshader, type, name, location);
			}
		}
	}

	/*--------------------------------------------------------------------------*/

	ROpenGL::Compile(glshader);

	technique.m_shaders.push_back(glshader);
}

/*--------------------------------------------------------------------------*/

uint Material::GetContextFlags(EMaterialTechnique::Type v) const
{
	if (this->m_techniques[(uint) v].m_context)
	{
		return this->m_techniques[(uint) v].m_context->m_flags;
	}
	else
	{
		AssertLogC(true, "Technique %d not found in %s", v, this->m_name.c_str());
		return 0;
	}
}

/*--------------------------------------------------------------------------*/

const Technique* Material::GetTechnique(EMaterialTechnique::Type v) const
{
	for (auto iter = this->m_techniques.begin(); iter != this->m_techniques.end(); ++iter)
	{
		if (iter->m_techniqueType == v)
		{
			return &(*iter);
		}
	}

	return NULL;
}

/*--------------------------------------------------------------------------*/